﻿using EcoTourEduca.BingYourMaps.Models;
using EcoTourEduca.BingYourMaps.ViewModels;
using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.Exceptions;
using System.Web.Mvc;
using Orchard.Localization;

namespace EcoTourEduca.BingYourMaps.Services
{
    public class BingYourMapsService : IBingYourMapsService
    {
        public const string MAPICONS_MEDIA_FOLDER = "mapicons";
        // TODO: move these to a config file the user can configure at runtime
        public const double DefaultLatitude = 37.219047;
        public const double DefaultLongitude = -4.531175;
        public const int DefaultZoom = 8;

        private readonly IContentManager _contentManager;
        private IRepository<BingMapsLocationAreaMappingRecord> _recordMappingsRepository;
    
        public BingYourMapsService(IContentManager contentManager, 
                            IRepository<BingMapsLocationAreaMappingRecord> mappingRecordRepository)
        {
            _contentManager = contentManager;
            _recordMappingsRepository = mappingRecordRepository;
        }

        #region IBingYourMapsService Members

        public string DefaultMapType
        {
            get { return "r"; }
        }

        public SelectList CreateMapTypeList(string mapType)
        {
            var mapDictionary = new Dictionary<String, String>
			{
				{"Auto", "auto"},
				{"Aerial", "a"},
				{"Birdseye", "be"},
				//{"Mercator", "m"}, // Shows blank when I use it
				{"Road", "r"},
				//{"Collins Bart", "cb"} en-gb only
				//{"Ordnance Survey", "os"} en-gb only
			};

            return new SelectList(mapDictionary, "Value", "Key", mapType);
        }

        public string GetTitle(IContent content, string defaultText)
        {
            if (content == null)
            {
                return null;
            }

            var title = 
                _contentManager.GetItemMetadata(content)
                .DisplayText;
            return title ?? String.Format(defaultText, content.Id);

            /* Could also look for name field if there is no route
            return bingMapList.ContentItem.Parts
                .SelectMany(p => p.Fields)
                .Where(f => f.Name == "Name")
                .First()
                .Storage.Get<string>(null);*/
        }

        public BingMapsAreaPart GetBingMapsAreaByPartId(int areaPartId)
        {
            return _contentManager
                    .Query<BingMapsAreaPart>()
                    .List()
                    .Where(area => area.Id == areaPartId)
                    .FirstOrDefault(); // default = null if id doesn't exist
        }

        public BingMapsAreaPart GetBingMapsAreaByRecordId(int areaRecordId)
        {
            return _contentManager
                .Query<BingMapsAreaPart>()
                .List()
                .Where(areaPart => areaPart.Record.Id == areaRecordId)
                .FirstOrDefault(); // default = null if id doesn't exist
        }

        public BingMapsLocationPart GetBingMapsLocationByPartId(int locationPartId)
        {
            return _contentManager
                    .Query<BingMapsLocationPart>()
                    .List()
                    .Where(location => location.Id == locationPartId)
                    .FirstOrDefault(); // default = null if id doesn't exist
        }

        public BingMapsLocationPart GetBingMapsLocationByRecordId(int locationRecordId)
        {
            return _contentManager
                .Query<BingMapsLocationPart>()
                .List()
                .Where(locationPart => locationPart.Record.Id == locationRecordId)
                .FirstOrDefault(); // default = null if id doesn't exist
        }

        public IEnumerable<BingMapsAreaViewModel> GetBingMapsAreasForLocationPart(int locationPartId)
        {
            try
            {
                // Retrieve list of BingMapsArea mapped to the BingMapsLocation
                BingMapsLocationPart locationPart = GetBingMapsLocationByPartId(locationPartId);
                if (locationPart == null)
                {
                    return null;
                }
                IEnumerable<int> mappedAreaRecordsIds =
                _recordMappingsRepository
                    .Fetch(mapping => mapping.BingMapsLocationRecord == locationPart.Record)
                    .Select(mapping => mapping.BingMapsAreaRecord.Id);

                // Fetch ALL BingMapsAreas and return them as view models
                return _contentManager
                    .Query<BingMapsAreaPart>()
                    .List()
                    .ToList()
                    .Select(areaPart =>
                        new BingMapsAreaViewModel()
                        {
                            PartId = areaPart.Id,
                            FriendlyName = areaPart.FriendlyName,
                            IsMapped = mappedAreaRecordsIds.Contains(areaPart.Record.Id)
                        });
            }
            catch (Exception e)
            {
                if (e.IsFatal())
                {
                    throw e;
                }
                else
                {
                    throw new OrchardException(
                        new LocalizedString("An error occurred when attempting to query all BingMaps Areas for a Location")
                        , e);
                }
            }
        }

        public IEnumerable<BingMapsAreaViewModel> GetAllBingMapsAreas()
        {
            try
            {
                // Fetch ALL BingMapsAreas and return them as view models
                return _contentManager
                    .Query<BingMapsAreaPart>()
                    .List()
                    .ToList()
                    .Select(areaPart =>
                        new BingMapsAreaViewModel()
                        {
                            PartId = areaPart.Id,
                            FriendlyName = areaPart.FriendlyName,
                            IsMapped = false
                        });
            }
            catch (Exception e)
            {
                if (e.IsFatal())
                {
                    throw e;
                }
                else
                {
                    throw new OrchardException(
                        new LocalizedString("An error occurred when attempting to query all BingMaps Areas for a Location")
                        , e);
                }
            }
        }

        public IEnumerable<BingMapsAreaPart> GetBingMapsAreaPartsForLocationPart(int locationPartId)
        {
            try
            {
                // Retrieve list of BingMapsArea mapped to the BingMapsLocation
                BingMapsLocationPart locationPart = GetBingMapsLocationByPartId(locationPartId);
                if (locationPart == null)
                {
                    return null;
                }
                IEnumerable<int> mappedAreasRecordsIds =
                _recordMappingsRepository
                    .Fetch(mapping => mapping.BingMapsLocationRecord == locationPart.Record)
                    .Select(mapping => mapping.BingMapsAreaRecord.Id);

                return mappedAreasRecordsIds
                    .ToList()
                    .Select(id => GetBingMapsAreaByRecordId(locationPartId));
            }
            catch (OrchardException e)
            {
                if (e.IsFatal())
                {
                    throw e;
                }
                else
                {
                    // TODO?
                    return null;
                }
            }
        }

        public IEnumerable<BingMapsLocationPart> GetBingMapsLocationPartsForAreaPart(int areaPartId)
        {
            try
            {
                // Retrieve mapped locations ids
                BingMapsAreaPart areaPart = GetBingMapsAreaByPartId(areaPartId);
                if (areaPart == null)
                {
                    return null;
                }
                IEnumerable<int> mappedLocationsRecordsIds =
                    _recordMappingsRepository
                    .Fetch(mapping => mapping.BingMapsAreaRecord == areaPart.Record)
                    .Select(mapping => mapping.BingMapsLocationRecord.Id);

                return mappedLocationsRecordsIds
                    .ToList()
                    .Select(id => GetBingMapsLocationByRecordId(id));
            }
            catch (OrchardException e)
            {
                if (e.IsFatal())
                {
                    throw e;
                }
                else
                {
                    // TODO?
                    return null;
                }
            }
        }

        // TODO: To make the code more efficient, only relationships to be removed/added should be removed/added. This version of the code
        // removes all existing relationships for the location and then adds it everywhere that corresponds.
        public void UpdateBingMapAreas(BingMapsLocationPart locationPart, EditBingMapsLocationViewModel viewModel)
        {
            if (locationPart != null && viewModel.BingMapAreas != null)
            {
                try
                {
                    // Get ids for attached BingMapAreaRecords
                    IEnumerable<int> toAttachBingMapAreasPartsIds = 
                    viewModel
                        .BingMapAreas
                        .Where(vmArea => vmArea.IsMapped)
                        .Select(vmArea => vmArea.PartId);

                    // Remove all previously applied mappings for the BingMapsLocation
                    IEnumerable<BingMapsLocationAreaMappingRecord> existingMappingsForLocation = 
                    _recordMappingsRepository
                        .Fetch(mapping => mapping.BingMapsLocationRecord == locationPart.Record); // record to record comparison
                    existingMappingsForLocation
                        .ToList()
                        .ForEach(record => _recordMappingsRepository.Delete(record));

                    // Add new Mappings
                    toAttachBingMapAreasPartsIds
                        .ToList()
                        .ForEach(mappedAreaId => _recordMappingsRepository.Create(
                            new BingMapsLocationAreaMappingRecord()
                            {
                                BingMapsLocationRecord  = locationPart.Record,
                                BingMapsAreaRecord      = GetBingMapsAreaByPartId(mappedAreaId).Record
                            }));
                }
                catch (OrchardException e)
                {
                    if (e.IsFatal())
                    {
                        throw e;
                    }
                    else
                    {
                        // TODO?
                    }
                }
            }
        }

        public bool IsLocationMappedToArea(BingMapsLocationRecord locationRecord, BingMapsAreaRecord areaRecord)
        {
            return _recordMappingsRepository
                .Fetch(mapping =>
                    mapping.BingMapsLocationRecord == locationRecord 
                    && mapping.BingMapsAreaRecord == areaRecord)
                .FirstOrDefault() != null;
        }

        public bool IsLocationMappedToArea(BingMapsLocationPart locationPart, BingMapsAreaPart areaPart)
        {
            return IsLocationMappedToArea(locationPart.Record, areaPart.Record);
        }

        public bool IsLocationMappedToArea(BingMapsLocationRecord locationRecord, BingMapsAreaPart areaPart)
        {
            return IsLocationMappedToArea(locationRecord, areaPart.Record);
        }

        public bool IsLocationMappedToArea(BingMapsLocationPart locationPart, BingMapsAreaRecord areaRecord)
        {
            return IsLocationMappedToArea(locationPart.Record, areaRecord);
        }

        public bool ExistsBingMapsAreaPart(int areaPartId)
        {
            BingMapsAreaPart areaPart = GetBingMapsAreaByPartId(areaPartId);
            return areaPart != null;
        }

        public bool ExistsBingMapsLocationPart(int locationPartId)
        {
            BingMapsLocationPart locationPart = GetBingMapsLocationByPartId(locationPartId);
            return locationPart != null;
        }

        public void RemoveMappingsForBingMapsArea(BingMapsAreaPart areaPart)
        {
            if (areaPart == null)
            {
                throw new ArgumentException("areaPart cannot be null");
            }
            try
            {
                IEnumerable<BingMapsLocationAreaMappingRecord> toRemoveMappings =
                    _recordMappingsRepository
                    .Fetch(mapping => mapping.BingMapsAreaRecord == areaPart.Record);
                toRemoveMappings
                    .ToList()
                    .ForEach(record => _recordMappingsRepository.Delete(record));
            }
            catch (OrchardException e)
            {
                if (e.IsFatal())
                {
                    throw e;
                }
                else
                {
                    // TODO?
                }
            }
        }

        public void RemoveMappingsForBingMapsLocation(BingMapsLocationPart locationPart)
        {
            if (locationPart == null)
            {
                throw new ArgumentException("areaPart cannot be null");
            }
            try
            {
                IEnumerable<BingMapsLocationAreaMappingRecord> toRemoveMappings =
                    _recordMappingsRepository
                    .Fetch(mapping => mapping.BingMapsLocationRecord == locationPart.Record);
                toRemoveMappings
                    .ToList()
                    .ForEach(record => _recordMappingsRepository.Delete(record));
            }
            catch (OrchardException e)
            {
                if (e.IsFatal())
                {
                    throw e;
                }
                else
                {
                    // TODO?
                }
            }
        }

        #endregion
    }
}